Skip to main content

media_pp\elements\driver\webrtc/
peer.rs

1use std::{
2    collections::HashMap,
3    net::UdpSocket,
4    sync::{
5        Arc, Mutex,
6        atomic::{AtomicU64, Ordering},
7    },
8    time::{Duration, Instant},
9};
10
11use crate::pp_log::{PpLog, pp_error, pp_info};
12use crossbeam_channel::{Receiver, Sender, TrySendError, bounded, unbounded};
13use ffmpeg_next as ffmpeg;
14use str0m::{
15    Event, Input, Output, Rtc,
16    change::{SdpOffer, SdpPendingOffer},
17    format::Codec,
18    media::{MediaKind, MediaTime, Mid},
19    net::{Protocol, Receive},
20};
21
22use crate::{
23    buffer::MediaBuffer,
24    bus::{Bus, BusEvent},
25    driver::{Driver, StopReceiver},
26    element::{Element, ElementType, element_pp_log},
27    error::Result,
28    time::{InvalidTimeBase, MediaTimestamp},
29};
30
31use super::{
32    command::{Command, TrackId, TrackOutState, WebRtcError},
33    track::{WebRtcHandle, WebRtcTrackSink, WebRtcTrackSource},
34};
35
36/// How often `WebRtcPeer::run` re-checks `stop`/its command channel while
37/// otherwise blocked on the UDP socket — see its own docs for why this is
38/// polling rather than a true multi-way wait.
39const POLL_INTERVAL: Duration = Duration::from_millis(20);
40
41/// Bound on the command channel (see [`Command`]) and on each attached
42/// track's inbound buffer (`WebRtcPeer` -> its `WebRtcTrackSource`). Once
43/// this many media buffers are backed up, the newest one is dropped
44/// instead of piling up in memory forever — the right call for live media,
45/// where a backed-up peer means falling behind, not something worth
46/// buffering indefinitely for (same reasoning as
47/// [`crate::queue::OverflowPolicy::DropNewest`]). Control traffic on the
48/// same channel (`AddTrack`/`SetAnswer`/`AcceptOffer`) is never dropped for
49/// capacity pressure — those call sites block on plain `send` instead of
50/// `try_send`. A disconnected peer still rejects the command; `set_answer`
51/// intentionally treats that case as a no-op.
52const CHANNEL_CAPACITY: usize = 128;
53/// The [`Driver`] — owns the [`Rtc`] session and its [`UdpSocket`], and
54/// drives str0m's sans-I/O poll loop on the dedicated thread
55/// [`crate::driver::DriverRunner::run`] gives it. Not a
56/// [`crate::element::SourceElement`]/[`crate::element::Source`]: it has no `src_pads()`
57/// dataflow graph of its own — see [`Driver`]'s own docs for why a
58/// connection with dynamically-appearing, independently bidirectional
59/// tracks doesn't fit that shape. Whatever it produces or consumes flows
60/// through the separate [`WebRtcTrackSink`]/[`WebRtcTrackSource`] pairs it
61/// mints per track instead (see below).
62///
63/// `rtc`/`socket` must already be connected: the initial SDP offer/answer
64/// and ICE candidate setup happen via str0m directly, in the caller's own
65/// code, *before* [`WebRtcPeer::new`]. `WebRtcPeer` only takes over after
66/// signaling has established the connection; it does not provide a signaling
67/// server itself.
68///
69/// Every track — whether it's one this side requested via
70/// [`WebRtcHandle::add_track`] or one the remote peer added (`str0m`'s
71/// `Event::MediaAdded`, which — critically — *never fires for a track this
72/// side added itself*) — is attached the same way, the moment its `Mid`
73/// exists: a [`WebRtcTrackSink`] (to reply on) and a [`WebRtcTrackSource`]
74/// (whatever the remote side sends on it) are minted together and handed
75/// out through [`WebRtcHandle::next_track`], no closure required. A single
76/// `Direction::SendRecv` track therefore needs exactly one
77/// [`WebRtcHandle::add_track`] call (on either side) and one
78/// `next_track()` on *each* side — no separate outbound API, and no
79/// special-casing for which side happened to originate it.
80/// (`Direction::SendOnly`/`RecvOnly` still work the same way; the unused
81/// half of the pair — a `WebRtcTrackSource` nothing ever sends on, or a
82/// `WebRtcTrackSink` str0m has no send capability for — is simply inert,
83/// not an error.) This is the same idea as
84/// [`crate::elements::TeeHandle::attach`]'s dynamic attachment, just
85/// without `Tee`'s `Mutex` (nothing but this one thread ever touches
86/// `tracks_in`).
87pub struct WebRtcPeer {
88    pp_log: PpLog,
89    name: Arc<str>,
90    rtc: Rtc,
91    socket: UdpSocket,
92    /// Where inbound data for each attached track goes: just a plain
93    /// `Sender`, not a `Box<dyn Sink>` — the matching `Receiver` lives
94    /// inside that track's own [`WebRtcTrackSource`], driven by *its own*
95    /// `Pipeline` on its own thread, so nothing here needs to know about
96    /// `ControlMsg` at all. The `Mutex` alongside it is the same
97    /// `WebRtcTrackSource`'s [`WebRtcTrackSource::codec`] cell — written
98    /// here (from `Event::MediaData`), read there, from whatever thread the
99    /// caller checks it on. It's the one piece of `tracks_in` shared across
100    /// threads; the map itself still isn't (see below).
101    #[allow(clippy::type_complexity)]
102    tracks_in: HashMap<Mid, (Sender<MediaBuffer>, Arc<Mutex<Option<Codec>>>)>,
103    tracks_out: HashMap<TrackId, TrackOutState>,
104    /// The codec each `tracks_out` entry was opened with — what
105    /// [`WebRtcHandle::add_track`] was told to expect, consulted by
106    /// `write_track` to pick the payload type matching what's actually
107    /// being pushed instead of guessing at whatever this connection
108    /// happened to negotiate first for that `Mid`.
109    track_codec: HashMap<TrackId, Codec>,
110    /// The one SDP exchange currently in flight (str0m only allows one at a
111    /// time — see `chat.rs`'s own `pending.is_some()` guard), plus which
112    /// `TrackId`s it covers, so [`Command::SetAnswer`] knows which entries
113    /// in `tracks_out` to flip from `Negotiating` to `Open`.
114    pending: Option<(SdpPendingOffer, Vec<TrackId>)>,
115    /// Shared with every [`WebRtcHandle`] clone, so `TrackId`s minted here
116    /// (for tracks the *remote* peer added — see the type docs) never
117    /// collide with ones `WebRtcHandle::add_track` mints.
118    next_id: Arc<AtomicU64>,
119    /// Cloned into every [`WebRtcTrackSink`] this element hands out via
120    /// [`WebRtcPeer::attach_track`] — including for tracks *this* side
121    /// requested, since `WebRtcTrackSink` is otherwise only ever
122    /// constructed from inside `run`.
123    command_tx: Sender<Command>,
124    command_rx: Receiver<Command>,
125    /// The other half of [`WebRtcHandle::next_track`] — one entry per
126    /// newly-attached track, in attachment order (see [`TrackId`]'s own
127    /// docs for why the caller has to match on it, not just take these in
128    /// order, when more than one track can appear).
129    new_track_tx: Sender<(TrackId, Mid, MediaKind, WebRtcTrackSink, WebRtcTrackSource)>,
130    on_offer: Box<dyn FnMut(SdpOffer) + Send>,
131    on_keyframe_request: Box<dyn FnMut(TrackId) + Send>,
132}
133impl WebRtcPeer {
134    /// `rtc`/`socket` must already be connected — see the type-level docs.
135    /// `on_offer` receives every renegotiation offer this element generates
136    /// (via [`WebRtcHandle::add_track`]) for the caller to ship over its
137    /// own signaling transport; `on_keyframe_request` reports which
138    /// outbound track the remote peer wants a keyframe for (forward this to
139    /// whatever's encoding that track). Newly-attached tracks themselves
140    /// come from [`WebRtcHandle::next_track`], not a constructor argument.
141    pub fn new(
142        name: impl Into<String>,
143        rtc: Rtc,
144        socket: UdpSocket,
145        on_offer: impl FnMut(SdpOffer) + Send + 'static,
146        on_keyframe_request: impl FnMut(TrackId) + Send + 'static,
147    ) -> (Self, WebRtcHandle) {
148        let name: Arc<str> = name.into().into();
149        let pp_log = element_pp_log(ElementType::WebRtcPeer, &name, None);
150        pp_info!(
151            pp_log: &pp_log,
152            "created: local_addr={:?}",
153            socket.local_addr()
154        );
155        let (command_tx, command_rx) = bounded(CHANNEL_CAPACITY);
156        let (new_track_tx, new_track_rx) = unbounded();
157        let next_id = Arc::new(AtomicU64::new(0));
158        (
159            Self {
160                name,
161                pp_log,
162                rtc,
163                socket,
164                tracks_in: HashMap::new(),
165                tracks_out: HashMap::new(),
166                track_codec: HashMap::new(),
167                pending: None,
168                next_id: next_id.clone(),
169                command_tx: command_tx.clone(),
170                command_rx,
171                new_track_tx,
172                on_offer: Box::new(on_offer),
173                on_keyframe_request: Box::new(on_keyframe_request),
174            },
175            WebRtcHandle {
176                next_id,
177                command_tx,
178                new_track_rx,
179            },
180        )
181    }
182
183    /// Mints a fresh [`WebRtcTrackSink`]/[`WebRtcTrackSource`] pair for
184    /// `mid`/`kind` and hands both out via [`WebRtcHandle::next_track`] —
185    /// see the type docs for why this is the one path both locally- and
186    /// remotely-added tracks go through.
187    fn attach_track(&mut self, id: TrackId, mid: Mid, kind: MediaKind) {
188        pp_info!(self, "track attached: id={id:?}, mid={mid}, kind={kind:?}");
189        let reply = WebRtcTrackSink::new(id, self.command_tx.clone());
190        let (tx, rx) = bounded(CHANNEL_CAPACITY);
191        let codec = Arc::new(Mutex::new(None));
192        self.tracks_in.insert(mid, (tx, codec.clone()));
193        let source = WebRtcTrackSource::new(format!("webrtc-track-{}-in", id.0), rx, codec);
194        let _ = self.new_track_tx.send((id, mid, kind, reply, source));
195    }
196
197    fn apply_command(&mut self, cmd: Command, bus: &Bus) -> Result<()> {
198        match cmd {
199            Command::AddTrack(id, kind, direction, codec) => {
200                pp_info!(
201                    self,
202                    "add_track requested: id={id:?}, kind={kind:?}, direction={direction:?}, codec={codec:?}"
203                );
204                self.tracks_out
205                    .insert(id, TrackOutState::ToOpen(kind, direction));
206                self.track_codec.insert(id, codec);
207            }
208            Command::Push(id, buf) => {
209                // A malformed media packet is local to this one track and
210                // buffer. Report and drop it without tearing down the
211                // entire live WebRTC connection, matching Queue's
212                // consume-error contract.
213                if let Err(error) = self.write_track(id, buf) {
214                    bus.post(
215                        &self.pp_log,
216                        BusEvent::Error {
217                            element_type: ElementType::WebRtcPeer,
218                            name: self.name.clone(),
219                            error,
220                        },
221                    );
222                }
223            }
224            Command::SetAnswer(answer) => {
225                let Some((pending, ids)) = self.pending.take() else {
226                    return Ok(());
227                };
228                self.rtc
229                    .sdp_api()
230                    .accept_answer(pending, answer)
231                    .inspect_err(|error| pp_error!(self, "accept_answer failed: {error}"))
232                    .map_err(WebRtcError::from)?;
233                pp_info!(self, "renegotiation complete: {} track(s)", ids.len());
234                for id in ids {
235                    if let Some(state @ TrackOutState::Negotiating(_)) = self.tracks_out.get(&id) {
236                        let mid = state.mid().expect("Negotiating always carries a Mid");
237                        self.tracks_out.insert(id, TrackOutState::Open(mid));
238                    }
239                }
240            }
241            Command::AcceptOffer(offer, reply) => {
242                let result = self
243                    .rtc
244                    .sdp_api()
245                    .accept_offer(offer)
246                    .inspect_err(|error| pp_error!(self, "accept_offer failed: {error}"))
247                    .map_err(WebRtcError::from);
248                if result.is_ok() {
249                    pp_info!(self, "accepted remote offer");
250                }
251                let _ = reply.send(result);
252            }
253        }
254        Ok(())
255    }
256
257    /// Starts a new SDP exchange if any track is waiting to be opened and
258    /// none is already in flight (str0m only allows one pending offer at a
259    /// time).
260    fn negotiate_if_needed(&mut self) {
261        if self.pending.is_some() {
262            return;
263        }
264        let to_open: Vec<TrackId> = self
265            .tracks_out
266            .iter()
267            .filter(|(_, s)| matches!(s, TrackOutState::ToOpen(..)))
268            .map(|(id, _)| *id)
269            .collect();
270        if to_open.is_empty() {
271            return;
272        }
273
274        let mut newly_negotiating = Vec::with_capacity(to_open.len());
275        let mut api = self.rtc.sdp_api();
276        for &id in &to_open {
277            let Some(TrackOutState::ToOpen(kind, direction)) = self.tracks_out.get(&id) else {
278                continue;
279            };
280            let (kind, direction) = (*kind, *direction);
281            let mid = api.add_media(kind, direction, None, None, None);
282            self.tracks_out.insert(id, TrackOutState::Negotiating(mid));
283            newly_negotiating.push((id, mid, kind));
284        }
285
286        if let Some((offer, pending)) = api.apply() {
287            pp_info!(self, "renegotiation started: {} track(s)", to_open.len());
288            self.pending = Some((pending, to_open));
289            (self.on_offer)(offer);
290        }
291
292        // str0m never fires `Event::MediaAdded` for media *this side* just
293        // added (see the type docs) — so this is the only place these
294        // newly-minted `Mid`s ever reach `attach_track`, unlike the remote
295        // side's own `Event::MediaAdded` handling below.
296        for (id, mid, kind) in newly_negotiating {
297            self.attach_track(id, mid, kind);
298        }
299    }
300
301    fn write_track(&mut self, id: TrackId, buf: MediaBuffer) -> Result<()> {
302        let Some(TrackOutState::Open(mid)) = self.tracks_out.get(&id) else {
303            // Not open yet (or unknown/never added) — dropped, see
304            // `WebRtcHandle::add_track`'s docs.
305            return Ok(());
306        };
307        let MediaBuffer::Packet(packet) = buf else {
308            return Ok(()); // Eos: nothing to write, nothing to flush
309        };
310        let Some(writer) = self.rtc.writer(*mid) else {
311            return Ok(());
312        };
313        // Only a locally-`add_track`ed track has a declared codec (the
314        // caller told us what it intends to push — see `add_track`'s
315        // docs). A remotely-added one (`Event::MediaAdded`) has no such
316        // declaration available at attach time, so this falls back to
317        // whatever this connection negotiated first for the `Mid` — same
318        // best-effort guess this always made, just now scoped to only the
319        // case that has no better option.
320        let pt = match self.track_codec.get(&id) {
321            Some(&codec) => writer.payload_params().find(|p| p.spec().codec == codec),
322            None => writer.payload_params().next(),
323        }
324        .map(|p| p.pt());
325        let Some(pt) = pt else {
326            return Ok(()); // no negotiated codec (matching or otherwise) yet
327        };
328        let data = packet.data().unwrap_or(&[]).to_vec();
329        let rtp_time = packet_rtp_time(&packet)?;
330        writer
331            .write(pt, Instant::now(), rtp_time, data)
332            .inspect_err(|error| pp_error!(self, "writer.write failed: {error}"))
333            .map_err(WebRtcError::from)?;
334        Ok(())
335    }
336
337    /// Drains every immediately-available str0m output (retransmits and
338    /// events), returning once str0m itself has nothing left to do until
339    /// the returned deadline.
340    fn drive_until_timeout(&mut self, bus: &Bus) -> Result<Instant> {
341        loop {
342            let output = self
343                .rtc
344                .poll_output()
345                .inspect_err(|error| pp_error!(self, "poll_output failed: {error}"))
346                .map_err(WebRtcError::from)?;
347            match output {
348                Output::Timeout(deadline) => return Ok(deadline),
349                Output::Transmit(t) => {
350                    // A single failed send (e.g. transient ICMP unreachable)
351                    // isn't fatal to the whole connection — str0m's own
352                    // retransmit/timeout logic handles loss.
353                    let _ = self.socket.send_to(&t.contents, t.destination);
354                }
355                Output::Event(event) => self.handle_event(event, bus),
356            }
357        }
358    }
359
360    fn handle_event(&mut self, event: Event, bus: &Bus) {
361        match event {
362            Event::MediaAdded(added) => {
363                // Only reached for media the *remote* peer added (see the
364                // type docs) — by definition already fully negotiated by
365                // the time we see this, so `Open` immediately: unlike a
366                // locally-requested track, there's no answer left to wait
367                // for before a `WebRtcTrackSink` bound to it can actually
368                // send.
369                let id = TrackId(self.next_id.fetch_add(1, Ordering::Relaxed));
370                self.tracks_out.insert(id, TrackOutState::Open(added.mid));
371                self.attach_track(id, added.mid, added.kind);
372            }
373            Event::MediaData(data) => {
374                if let Some((tx, codec)) = self.tracks_in.get(&data.mid) {
375                    // Every packet, not just the first: cheap (one lock),
376                    // and correct if the remote side ever actually changes
377                    // codec mid-stream (rare, but the payload type is free
378                    // to vary packet-to-packet — see `WebRtcTrackSource::
379                    // codec`'s own docs for why this can't be pinned down
380                    // any earlier than "whatever the last packet said").
381                    *codec.lock().unwrap() = Some(data.params.spec().codec);
382
383                    let mut packet = ffmpeg::Packet::copy(&data.data);
384                    // `data.time` is str0m's own RTP timestamp (numerator)
385                    // over the codec's clock rate (denominator) — reused
386                    // as-is for pts/dts. No B-frame reordering happens over
387                    // RTP (decode order == transmit order), so pts and dts
388                    // are always the same value here.
389                    packet.set_time_base(ffmpeg::Rational::new(1, data.time.denom() as i32));
390                    let pts = data.time.numer() as i64;
391                    packet.set_pts(Some(pts));
392                    packet.set_dts(Some(pts));
393                    if data.is_keyframe() {
394                        let flags = packet.flags() | ffmpeg::codec::packet::Flags::KEY;
395                        packet.set_flags(flags);
396                    }
397                    match tx.try_send(MediaBuffer::Packet(Arc::new(packet))) {
398                        Ok(()) => {}
399                        Err(TrySendError::Full(_)) => {
400                            // This track's `WebRtcTrackSource` (or whatever
401                            // it feeds) isn't keeping up — drop the newest
402                            // buffer rather than let this grow unbounded
403                            // (see `CHANNEL_CAPACITY`'s docs).
404                            bus.post(
405                                &self.pp_log,
406                                BusEvent::Dropped {
407                                    element_type: ElementType::WebRtcPeer,
408                                    name: self.name.clone(),
409                                },
410                            );
411                        }
412                        Err(TrySendError::Disconnected(_)) => {
413                            // This track's `WebRtcTrackSource` is gone (its
414                            // own `Pipeline` finished) — stop trying to feed
415                            // it.
416                            self.tracks_in.remove(&data.mid);
417                        }
418                    }
419                }
420            }
421            Event::KeyframeRequest(req) => {
422                if let Some((&id, _)) = self
423                    .tracks_out
424                    .iter()
425                    .find(|(_, s)| s.mid() == Some(req.mid))
426                {
427                    pp_info!(self, "keyframe requested: id={id:?}, mid={}", req.mid);
428                    (self.on_keyframe_request)(id);
429                }
430            }
431            Event::Connected => {
432                pp_info!(self, "ICE+DTLS connected");
433            }
434            Event::IceConnectionStateChange(state) => {
435                pp_info!(self, "ICE connection state: {state:?}");
436            }
437            Event::MediaChanged(changed) => {
438                pp_info!(
439                    self,
440                    "media changed: mid={}, direction={:?}",
441                    changed.mid,
442                    changed.direction
443                );
444            }
445            // `Event` is `#[non_exhaustive]` — data channels, stats, etc.
446            // are still outside this element's concern for now.
447            _ => {}
448        }
449    }
450}
451
452/// Converts a `Packet`'s `(pts, time_base)` into the `MediaTime` str0m
453/// expects for [`str0m::media::Writer::write`]. `MediaTime` is
454/// numer/denom *seconds* (str0m rebases it to the codec's RTP clock rate
455/// internally), but an FFmpeg `time_base` is numer/denom *seconds per
456/// tick* — so the elapsed time is `pts * numerator / denominator`, not
457/// `pts / denominator`. Most time bases in this codebase have numerator 1
458/// (e.g. `1/90_000`), which would hide a naive `pts / denominator`: an
459/// NTSC-style `1001/30_000` time base would make the RTP timestamp run
460/// ~1001x too fast.
461pub(super) fn packet_rtp_time(
462    packet: &ffmpeg::Packet,
463) -> std::result::Result<MediaTime, WebRtcError> {
464    let pts = packet.pts().ok_or(WebRtcError::MissingPacketPts)?;
465    let timestamp = MediaTimestamp::try_new(pts, packet.time_base()).map_err(
466        |InvalidTimeBase {
467             numerator,
468             denominator,
469         }| WebRtcError::InvalidPacketTimeBase {
470            numerator,
471            denominator,
472        },
473    )?;
474    to_str0m_media_time(timestamp)
475}
476
477/// Converts a validated `(pts, time_base)` into the `MediaTime` str0m
478/// expects for [`str0m::media::Writer::write`]. `MediaTime` is numer/denom
479/// *seconds* (str0m rebases it to the codec's RTP clock rate internally),
480/// but an FFmpeg `time_base` is numer/denom *seconds per tick* — so the
481/// elapsed time is `pts * numerator / denominator`, not `pts /
482/// denominator`. This keeps that exact `(pts * numerator, denominator)`
483/// rational rather than rescaling to some fixed target base first — a
484/// backend-specific conversion, so it lives here rather than on
485/// `MediaTimestamp` itself.
486fn to_str0m_media_time(timestamp: MediaTimestamp) -> std::result::Result<MediaTime, WebRtcError> {
487    let time_base = timestamp.time_base().get();
488    let numerator = time_base.numerator();
489    let denominator = time_base.denominator();
490    let pts = u64::try_from(timestamp.pts())
491        .map_err(|_| WebRtcError::NegativePacketPts(timestamp.pts()))?;
492    let frequency = str0m::media::Frequency::new(denominator as u32).ok_or(
493        WebRtcError::InvalidPacketTimeBase {
494            numerator,
495            denominator,
496        },
497    )?;
498    let numer = pts
499        .checked_mul(numerator as u64)
500        .ok_or(WebRtcError::PacketTimestampOverflow {
501            pts,
502            numerator,
503            denominator,
504        })?;
505    Ok(MediaTime::new(numer, frequency))
506}
507
508impl Element for WebRtcPeer {
509    fn name(&self) -> Arc<str> {
510        self.name.clone()
511    }
512
513    fn element_type(&self) -> ElementType {
514        ElementType::WebRtcPeer
515    }
516
517    fn pp_log(&self) -> &PpLog {
518        &self.pp_log
519    }
520
521    fn pp_log_mut(&mut self) -> &mut PpLog {
522        &mut self.pp_log
523    }
524}
525
526impl Driver for WebRtcPeer {
527    /// Drives str0m's poll loop. Every iteration: apply any commands from
528    /// `WebRtcHandle`/`WebRtcTrackSink`, start a renegotiation if a track
529    /// is waiting, drain str0m's own output (writing/dispatching as it
530    /// goes), check `stop`, then block on the UDP socket for at most
531    /// `POLL_INTERVAL` — capped below whatever str0m itself asked for, so
532    /// the command channel and `stop` are never starved for longer than
533    /// that even when nothing else is happening. There's no true
534    /// multi-way wait across the command channel, `stop`, *and* a raw
535    /// socket the way [`crate::elements::AppSource`] manages across two
536    /// `crossbeam_channel`s (a `UdpSocket` isn't `select!`-able), so this
537    /// is bounded polling instead — worst case `POLL_INTERVAL` of extra
538    /// latency for `Stop`/a fresh `add_track`, not unboundedly stuck.
539    ///
540    /// `stop`/the connection dying both clear `tracks_in` immediately, so
541    /// every already-handed-out `WebRtcTrackSource` sees its data channel
542    /// disconnect and ends with a final `Eos` right away, instead of
543    /// waiting for this whole `WebRtcPeer` to be dropped later by whatever
544    /// owns its `DriverRunner`. Neither `WebRtcPeer` nor its
545    /// `WebRtcTrackSource`s have a `Pause`/`Seek` concept — see
546    /// [`Driver`]'s own docs for why that's not just an oversight: freezing
547    /// this loop would starve ICE keepalives/DTLS retransmits, likely
548    /// dropping the connection rather than gracefully suspending it.
549    fn run(&mut self, stop: &StopReceiver, bus: &Bus) -> Result<()> {
550        pp_info!(self, "started");
551        let mut buf = vec![0u8; 2000];
552        loop {
553            while let Ok(cmd) = self.command_rx.try_recv() {
554                self.apply_command(cmd, bus)?;
555            }
556            self.negotiate_if_needed();
557
558            let deadline = self.drive_until_timeout(bus)?;
559            if !self.rtc.is_alive() || stop.is_stopped() {
560                pp_info!(self, "stopped rtc_alive={}", self.rtc.is_alive());
561                self.tracks_in.clear();
562                return Ok(());
563            }
564
565            let wait = deadline
566                .saturating_duration_since(Instant::now())
567                .min(POLL_INTERVAL)
568                .max(Duration::from_millis(1));
569            self.socket
570                .set_read_timeout(Some(wait))
571                .inspect_err(|error| pp_error!(self, "set_read_timeout failed: {error}"))
572                .map_err(WebRtcError::from)?;
573
574            match self.socket.recv_from(&mut buf) {
575                Ok((n, source)) => {
576                    let Ok(contents) = buf[..n].try_into() else {
577                        continue; // not a WebRTC datagram we recognize — ignore
578                    };
579                    let destination = self
580                        .socket
581                        .local_addr()
582                        .inspect_err(|error| pp_error!(self, "local_addr failed: {error}"))
583                        .map_err(WebRtcError::from)?;
584                    self.rtc
585                        .handle_input(Input::Receive(
586                            Instant::now(),
587                            Receive {
588                                proto: Protocol::Udp,
589                                source,
590                                destination,
591                                contents,
592                            },
593                        ))
594                        .inspect_err(|error| {
595                            pp_error!(self, "handle_input(Receive) failed: {error}")
596                        })
597                        .map_err(WebRtcError::from)?;
598                }
599                Err(e)
600                    if matches!(
601                        e.kind(),
602                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
603                    ) =>
604                {
605                    // Nothing arrived, but str0m still needs to be told
606                    // time has passed — its own internal clock only moves
607                    // forward via `Input::Timeout`, and *that* is what
608                    // makes the next `poll_output()` produce whatever's
609                    // next (retransmits, RTCP, the initial STUN checks,
610                    // ...). Skipping this on every timeout would leave
611                    // str0m stuck forever waiting for input that already
612                    // isn't coming.
613                    self.rtc
614                        .handle_input(Input::Timeout(Instant::now()))
615                        .inspect_err(|error| {
616                            pp_error!(self, "handle_input(Timeout) failed: {error}")
617                        })
618                        .map_err(WebRtcError::from)?;
619                }
620                Err(e) => {
621                    pp_error!(self, "recv_from failed: {e}");
622                    return Err(WebRtcError::from(e).into());
623                }
624            }
625        }
626    }
627}